/*
Copyright (C) 2016  Marien Raat

This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
published by the Free Software Foundation, either version 3 of the
License, or (at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU Affero General Public License for more details.

You should have received a copy of the GNU Affero General Public License
along with this program.  If not, see <http://www.gnu.org/licenses/>.
*/

#include <SFML/Graphics.hpp>
#include <SFML/Network.hpp>
#include <Box2D/Box2D.h>

#include <iostream>
#include <assert.h>
#include <vector>
#include <cmath>

#include "car.h"
#include "field.h"
#include "ball.h"
#include "aiManager.h"
#include "boostCounter.h"
#include "camera.h"
#include "config.h"
#include "networkDefinitions.h"

void loadFiles();
void initializeGame();
void initializeNetwork(int argc, char **argv);

void handleEvents();
void handleInput();
void updateNetwork();
void updateGame();
void draw();

sf::Socket::Status receiveNewPacket();
void updateFromPacket(sf::Packet &packet);
void sendUpdatePacket();
void sendLeavePacket();

void nextCamera();

sf::RenderWindow window;
sf::View gameView, guiView;
sf::Texture redCarTexture, blueCarTexture, fieldTexture,
    boostCounterOverlayTexture, boostCounterPointerTexture,
    carExhaustTexture, grassTexture;
sf::Font ubuntuMonoFont;
bool focus, keyboard;
int joystick;

std::vector<Camera*> cameras;
int currentCamera;

b2World world(b2Vec2(0, 0));
float box2DTimeStep = 1.0f / 60.0f;
int velocityIterations = 8
    , positionIterations = 3;

float timeStep = box2DTimeStep;
bool client = true;

int maxCars = 6;
Car *playerCars;
int playerNumber;
bool *playersConnected;
float power, turning;
bool boosting = false, drifting = false;
int dashDirection = -1;

Field field;
Ball ball;
AIManager aiManager;
BoostCounter boostCounter;

sf::UdpSocket socket;
sf::Packet currentPacket;
sf::IpAddress serverAddress, receiveAddress;
unsigned short serverPort, receivePort;

int main(int argc, char **argv)
{
    initializeNetwork(argc, argv);
    loadFiles();
    initializeGame();

    sf::Clock fpsTimer;
    int frames = 0;

    while (window.isOpen()) {
        if (fpsTimer.getElapsedTime().asSeconds() > 1.0) {
            std::cout << "FPS: " << frames << std::flush;
            fpsTimer.restart();
            frames = 0;
        }
        frames++;
        handleEvents();
        if (focus) handleInput();
        updateNetwork();
        updateGame();
        draw();
    }

    sendLeavePacket();
}

void loadFiles()
{
#define loadFromFile(object, file) \
    if (!((object).loadFromFile((file)))) {            \
        std::cout << "Couldn't load file " << file     \
                  << ". For object " << #object << "." \
                  << std::endl;                        \
        exit(-1);                                      \
    }

    loadFromFile(redCarTexture, "sprites/race_car_1_red.png");
    loadFromFile(blueCarTexture, "sprites/race_car_1_blue.png");
    loadFromFile(fieldTexture, "sprites/field_2.png");
    loadFromFile(boostCounterOverlayTexture,
                 "sprites/boost_counter.png");
    loadFromFile(boostCounterPointerTexture,
                 "sprites/boost_pointer.png");
    loadFromFile(ubuntuMonoFont, "fonts/UbuntuMono-B.ttf");
    loadFromFile(carExhaustTexture, "sprites/flame.png");
    loadFromFile(grassTexture, "sprites/grass_tile_1.png");
}

void initializeGame()
{
    focus = true;
    playerCars = new Car[maxCars];
    playersConnected = new bool[maxCars];
    for (int i = 0; i < maxCars; i++) {
        playerCars[i].initialize(&world);
        playerCars[i].setTextures((i % 2) ? &blueCarTexture
                                  : &redCarTexture,
                                  &carExhaustTexture);
        playersConnected[i] = false;
    }

    field.setTextures(&fieldTexture, &grassTexture);
    field.initialize(&world, FIELD_SIZE);

    ball.initialize(&world, BALL_RADIUS);

    aiManager.initialize(playerCars, playersConnected, maxCars,
                         &ball);

    boostCounter.initialize(&boostCounterOverlayTexture,
                            &boostCounterPointerTexture,
                            &ubuntuMonoFont);

    window.create(sf::VideoMode(1500, 900), "Car Soccer");
    guiView.setSize(1500, 900);

    currentCamera = 0;
    cameras.push_back(new PlayerCamera());
    cameras.push_back(new BallCamera());
    cameras.push_back(new FieldCamera());
    for (int i = 0; i < cameras.size(); i++)
        cameras[i]->setWindowSize(sf::Vector2f(1500, 900));

    window.setVerticalSyncEnabled(true);
}

void initializeNetwork(int argc, char **argv)
{
    char *ipString;
    if (argc > 1) {
        ipString = argv[1];
    } else {
        std::cout << "Input the servers IP address:" << std::endl;
        std::cin >> ipString;
    }
    serverAddress = sf::IpAddress(ipString);

    if (argc > 2) {
        serverPort = atoi(argv[2]);
    } else {
        std::cout << "Input the port on which the game is hosted"
                  << std::endl;
        std::cin >> serverPort;
    }

    sf::Packet joinPacket;
    joinPacket << ((sf::Int32) PacketJoin);
    socket.send(joinPacket, serverAddress,
                serverPort);

    sf::Packet assignPacket;
    socket.receive(assignPacket, receiveAddress, receivePort);
    sf::Int32 packetType;
    assignPacket >> packetType >> playerNumber;
    assert(packetType == PacketAssign);

    socket.setBlocking(false);
}

void handleEvents()
{
    sf::Event event;
    while(window.pollEvent(event)) {
        if (event.type == sf::Event::Closed) {
            window.close();
        }
        if (event.type == sf::Event::Resized) {
            guiView.setSize(event.size.width, event.size.height);
            for (int i = 0; i < cameras.size(); i++)
                cameras[i]->setWindowSize(
                    sf::Vector2f(event.size.width,
                                 event.size.height));
        }
        if (event.type == sf::Event::LostFocus) {
            focus = false;
        }
        if (event.type == sf::Event::GainedFocus) {
            focus = true;
        }
        if (event.type == sf::Event::KeyPressed) {
            keyboard = true;
            if (event.key.code == sf::Keyboard::C) {
                nextCamera();
            }
        }
        if (event.type == sf::Event::JoystickButtonPressed) {
            keyboard = false;
            joystick = event.joystickButton.joystickId;
            if (event.joystickButton.button == 3) {
                nextCamera();
            }
        }
        if (event.type == sf::Event::JoystickMoved) {
            keyboard = false;
            joystick = event.joystickMove.joystickId;
        }
    }
}

void handleInput()
{
    if (keyboard) {
        power = 0;
        turning = 0;
        if (sf::Keyboard::isKeyPressed(sf::Keyboard::Up) ||
            sf::Keyboard::isKeyPressed(sf::Keyboard::W))
            power += 1;
        if (sf::Keyboard::isKeyPressed(sf::Keyboard::Down) ||
            sf::Keyboard::isKeyPressed(sf::Keyboard::S))
            power -= 1;
        if (sf::Keyboard::isKeyPressed(sf::Keyboard::Left) ||
            sf::Keyboard::isKeyPressed(sf::Keyboard::A))
            turning -= 1;
        if (sf::Keyboard::isKeyPressed(sf::Keyboard::Right) ||
            sf::Keyboard::isKeyPressed(sf::Keyboard::D))
            turning += 1;
        boosting = sf::Keyboard::isKeyPressed(sf::Keyboard::LShift);
        drifting = sf::Keyboard::isKeyPressed(sf::Keyboard::LControl);
    } else {
        power = ((sf::Joystick::getAxisPosition
                  (joystick, sf::Joystick::R)) -
                 (sf::Joystick::getAxisPosition
                  (joystick, sf::Joystick::Z))) / 200.0f;
        turning = sf::Joystick::getAxisPosition
            (joystick, sf::Joystick::X) / 100.0f;
        // To stop accidental steering
        if (std::abs(turning) < 0.2f) turning = 0.0f;
        boosting = sf::Joystick::isButtonPressed(joystick, 1);
        drifting = sf::Joystick::isButtonPressed(joystick, 2);
        if (sf::Joystick::isButtonPressed(joystick, 0)) {
            bool horizontal = abs(sf::Joystick::getAxisPosition
                                  (joystick, sf::Joystick::X)) >
                abs(sf::Joystick::getAxisPosition
                    (joystick, sf::Joystick::Y));
            if (horizontal)
                dashDirection =
                    (sf::Joystick::getAxisPosition
                     (joystick, sf::Joystick::X) < 0.0f) ? 3 : 1;
            else
                dashDirection =
                    (sf::Joystick::getAxisPosition
                     (joystick, sf::Joystick::Y) < 0.0f) ? 0 : 2;
        } else
            dashDirection = -1;
    }
}

void updateGame()
{
    ball.update();
    for (int i = 0; i < maxCars; i++)
        playerCars[i].update();
    aiManager.update();

    world.Step(box2DTimeStep, velocityIterations, positionIterations);
    world.ClearForces();

    guiView.setCenter(guiView.getSize().x / 2,
                      guiView.getSize().y / 2);
    for (int i = 0; i < cameras.size(); i++)
        cameras[i]->update(playerCars[playerNumber].getPosition(),
                           ball.getPosition());
}

void updateNetwork()
{
    while (receiveNewPacket() == sf::Socket::Done) {
        sf::Packet packet = currentPacket;
        assert(receiveAddress == serverAddress &&
               receivePort == serverPort);

        receiveNewPacket();

        sf::Int32 packetType;
        packet >> packetType;

        if (packetType == PacketUpdate) {
            updateFromPacket(packet);
            sendUpdatePacket();
        }
    }
}

void draw()
{
    window.clear(sf::Color(80, 80, 80));
    window.setView(cameras[currentCamera]->getView());
    field.draw(&window);
    for (int i = 0; i < maxCars; i++) {
        playerCars[i].draw(&window);
    }
    ball.draw(&window);
    window.setView(guiView);
    boostCounter.draw(&window, playerCars[playerNumber].boostLeft);
    window.display();
}

sf::Socket::Status receiveNewPacket()
{
    return socket.receive(currentPacket, receiveAddress,
                                   receivePort);
}

void updateFromPacket(sf::Packet &packet)
{
    for (int i = 0; i < maxCars; i++) {
        packet >> playerCars[i] >> playersConnected[i];
    }
    packet >> ball;
}

void sendUpdatePacket()
{
    sf::Packet updatePacket;
    updatePacket << ((sf::Int32) PacketUpdate) << playerNumber
                 << power << turning << boosting << drifting
                 << dashDirection;
    socket.send(updatePacket, serverAddress, serverPort);
}

void sendLeavePacket()
{
    sf::Packet leavePacket;
    leavePacket << ((sf::Int32) PacketLeave) << playerNumber;
    socket.setBlocking(true);
    socket.send(leavePacket, serverAddress, serverPort);
}

void nextCamera()
{
    currentCamera++;
    if (currentCamera >= cameras.size())
        currentCamera = 0;
}
